package com.gitee.easyopen.support;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.interfaces.RSAPrivateKey;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.velocity.VelocityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.gitee.easyopen.ApiConfig;
import com.gitee.easyopen.ApiContext;
import com.gitee.easyopen.ApiResult;
import com.gitee.easyopen.Encrypter;
import com.gitee.easyopen.Invoker;
import com.gitee.easyopen.ParamNames;
import com.gitee.easyopen.RespWriter;
import com.gitee.easyopen.Result;
import com.gitee.easyopen.auth.Oauth2Manager;
import com.gitee.easyopen.auth.Oauth2Service;
import com.gitee.easyopen.bean.Consts;
import com.gitee.easyopen.bean.RequestMode;
import com.gitee.easyopen.doc.ApiDocBuilder;
import com.gitee.easyopen.doc.ApiDocHolder;
import com.gitee.easyopen.exception.ApiException;
import com.gitee.easyopen.interceptor.ApiInterceptor;
import com.gitee.easyopen.message.Errors;
import com.gitee.easyopen.monitor.MonitorApiInfo;
import com.gitee.easyopen.monitor.MonitorSearch;
import com.gitee.easyopen.monitor.MonitorStore;
import com.gitee.easyopen.register.AbstractInitializer;
import com.gitee.easyopen.util.MD5Util;
import com.gitee.easyopen.util.RequestUtil;
import com.gitee.easyopen.util.VelocityUtil;

/**
 * 提供API访问能力,新建一个类继承这个即可.RequestMapping中的value自己定义
 * 
 * <pre>
 * &#64;Controller
 * &#64;RequestMapping(value = "/api")
 * public class IndexController extends ApiController {
 * }
 * 
 * 这样接口的统一访问路径为:http://ip:port/contextPath/api
 * </pre>
 */
public abstract class ApiController extends AbstractInitializer implements ApplicationListener<ContextRefreshedEvent> {
    
    private static final String SESSION_KEY_MONITOR_PASSWORD = "session_key_monitor_password";
    
    protected ApiConfig apiConfig;
    
    @Autowired(required = false)
    protected Oauth2Manager oauth2Manager;
    
    protected Oauth2Service oauth2Service;
    
    protected Invoker invoker;

    // spring容器加载完毕后执行
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ApplicationContext applicationContext = getRootApplicationContext(event.getApplicationContext());
        ApiContext.setApplicationContext(applicationContext);
        
        this.apiConfig = newApiConfig();
        ApiContext.setApiConfig(apiConfig);
        
        apiConfig.loadPrivateKey();
        
        this.initApiConfig(this.apiConfig);

        this.init(applicationContext, this.apiConfig);
        
        this.initComponent(); // 放在最后
    }
    
    protected ApplicationContext getRootApplicationContext(ApplicationContext applicationContext) {
        ApplicationContext rootApplicationContext = applicationContext.getParent();
        return rootApplicationContext != null ? rootApplicationContext : applicationContext;
    }
    
    protected ApiConfig newApiConfig() {
        return new ApiConfig();
    }
    
    private void initComponent() {
        if(oauth2Manager != null) {
            apiConfig.initOauth2Service(oauth2Manager);
            oauth2Service = apiConfig.getOauth2Service();
        }
        invoker = apiConfig.getInvoker();
        
        initInterceptor();
    }
    
    // 添加监控拦截器
    private void initInterceptor() {
        if(this.apiConfig.isShowMonitor()) {
            ApiInterceptor[] interceptors = this.apiConfig.getInterceptors();
            int len = interceptors.length + 1;
            ApiInterceptor[] newInterceptors = new ApiInterceptor[len];
            newInterceptors[0] = this.apiConfig.getMonitorInerceptor(); // 监控拦截器放在首位
            for (int i = 0; i < interceptors.length; i++) {
                newInterceptors[i + 1] = interceptors[i];
            }
            this.apiConfig.setInterceptors(newInterceptors);
        }
    }
    
    /**
     * 请求入口
     * 
     * @param request
     * @param response
     * @throws Throwable
     */
    @RequestMapping(method = RequestMethod.POST)
    public void index(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        Object result = this.invoke(request, response);
        this.responseResult(response, result);
    }
    
    /**
     * 加密请求入口
     * @param request
     * @param response
     * @throws Throwable
     */
    @RequestMapping(value = "ssl", method = RequestMethod.POST)
    public void ssl(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        Object result = this.invokeBySSL(request, response);
        this.responseResult(response, result);
    }
    
    /* ********************** */
    // 这里将返回结果暴露在Controller层,之前是在ApiInvoker里面.
    // 这样做灵活一点,以后还可以做WebFlux
    /* ********************** */
    /**
     * 调用业务方法
     * @param request
     * @param response
     * @return 返回结果对象
     * @throws Throwable 
     */
    protected Object invoke(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        // 调用接口方法,即调用被@Api标记的方法
        ApiContext.setRequestMode(RequestMode.SIGNATURE);
        return this.invoker.invoke(request, response);
    }
    
    
    /**
     * 调用业务方法,加密模式
     * @param request
     * @param response
     * @return 返回结果对象
     * @throws Throwable 
     */
    protected Object invokeBySSL(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        ApiContext.setRequestMode(RequestMode.ENCRYPT);
        return this.invoker.invoke(request, response);
    }
    
    
    /**
     * 写数据到客户端
     * @param response
     * @param result 结果
     * @param format 返回类型，json,xml之一
     */
    public void responseResult(HttpServletResponse response, Object result) {
        RespWriter respWriter = this.apiConfig.getRespWriter();
        respWriter.write(response, result);
    }

    /**
     * 文档页面
     * 
     * @param request
     * @param response
     * @throws Throwable
     */
    @RequestMapping("doc")
    public void doc(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=UTF-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        if (!this.apiConfig.isShowDoc()) {
            response.getWriter().write("文档功能未开启");
            return;
        }

        ClassPathResource res = new ClassPathResource(this.apiConfig.getDocClassPath());

        VelocityContext context = new VelocityContext();
        context.put("title", "API文档");
        ApiDocBuilder docBuilder = ApiDocHolder.getApiDocBuilder();
        context.put("docEntrys", docBuilder.getDocItemEntry());
        String url = request.getRequestURL().toString();
        context.put("url", url.substring(0, url.length() - 4));
        
        context.put("ctx", request.getContextPath());
        
        context.put("ACCESS_TOKEN_NAME", ParamNames.ACCESS_TOKEN_NAME);
        context.put("API_NAME", ParamNames.API_NAME);
        context.put("APP_KEY_NAME", ParamNames.APP_KEY_NAME);
        context.put("DATA_NAME", ParamNames.DATA_NAME);
        context.put("FORMAT_NAME", ParamNames.FORMAT_NAME);
        context.put("SIGN_METHOD_NAME", ParamNames.SIGN_METHOD_NAME);
        context.put("SIGN_NAME", ParamNames.SIGN_NAME);
        context.put("TIMESTAMP_NAME", ParamNames.TIMESTAMP_NAME);
        context.put("TIMESTAMP_PATTERN", ParamNames.TIMESTAMP_PATTERN);
        context.put("VERSION_NAME", ParamNames.VERSION_NAME);
        
        this.processDocVelocityContext(context);

        VelocityUtil.generate(context, res.getInputStream(), response.getWriter());
    }
    
    /**
     * 对doc模板做额外处理
     * @param context
     */
    protected void processDocVelocityContext(VelocityContext context) {
        
    }
    
    /**
     * 进入监控页面
     * @param monitorSearch 查询参数
     * @return
     * @throws IOException 
     * @throws ServletException 
     */
    @RequestMapping("monitor")
    public void monitor(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        Object value = request.getSession().getAttribute(SESSION_KEY_MONITOR_PASSWORD);
        if(value != null || this.checkMonitorPassword(request)) {
            request.getSession().setAttribute(SESSION_KEY_MONITOR_PASSWORD, true);
            request.getRequestDispatcher("monitorPage").forward(request, response);
        } else {
            request.getSession().setAttribute(SESSION_KEY_MONITOR_PASSWORD, null);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html; charset=UTF-8");
            response.setHeader("Pragma", "No-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expires", 0);
            
            String ctx = request.getContextPath();
            
            String script = ("<script type='text/javascript' src='"+ctx+"/opendoc/md5.js'></script>").replace("'", "\"");
            StringBuilder html = new StringBuilder();
            html.append("<html><head>").append(script)
            .append("</head><body>")
            .append("<form style='display:none;' method='post' action='monitor'><input name='p' type='password' /></form>".replace("'", "\""))
            .append("<script>var pwd = null;while(!pwd) {pwd = prompt('请输入密码');};var frm = document.forms[0];frm.p.value = hex_md5(pwd);frm.submit();</script>")
            .append("</body></html>");
            
            response.getWriter().write(html.toString());
        }
    }
    
    
    private boolean checkMonitorPassword(HttpServletRequest request) {
        String inputPassword = request.getParameter("p");
        if(inputPassword == null) {
            return false;
        }
        String truePassword = MD5Util.encrypt(this.apiConfig.getMonitorPassword());
        return truePassword.equals(inputPassword);
    }
    
    @RequestMapping("monitorPage")
    public void monitorPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if(request.getSession().getAttribute(SESSION_KEY_MONITOR_PASSWORD) == null) {
            response.sendRedirect("monitor");
            return;
        }
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=UTF-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        if (!this.apiConfig.isShowMonitor()) {
            response.getWriter().write("监控功能未开启");
            return;
        }
        
        ClassPathResource res = new ClassPathResource(this.apiConfig.getMonitorClassPath());

        VelocityContext context = new VelocityContext();
        context.put("title", "监控");
        context.put("errorSize", this.apiConfig.getMonitorErrorQueueSize());
        context.put("ctx", request.getContextPath());
        
        this.processMonitorVelocityContext(context);
        
        VelocityUtil.generate(context, res.getInputStream(), response.getWriter());
    }
    
    /**
     * 对监控模板做额外处理
     * @param context
     */
    protected void processMonitorVelocityContext(VelocityContext context) {
        
    }
    
    // 查询监控数据
    @RequestMapping("monitorData")
    @ResponseBody
    public Object monitorData(MonitorSearch monitorSearch,HttpServletRequest request) throws IOException {
        if(request.getSession().getAttribute(SESSION_KEY_MONITOR_PASSWORD) == null) { // 无权限
            return -1;
        }
        MonitorStore store = this.apiConfig.getMonitorStore();
        long total = store.getTotal(monitorSearch);
        List<MonitorApiInfo> list = store.getList(monitorSearch);
        
        Map<String, Object> map = new HashMap<>();
        map.put("total", total);
        map.put("rows", list);
        
        return map;
    }
    
    // 删除监控数据
    @RequestMapping("monitorDel")
    @ResponseBody
    public Object monitorDel(String name,String version) throws IOException {
        MonitorStore store = this.apiConfig.getMonitorStore();
        store.clean(name, version);
        return Collections.emptyMap();
    }
    
    /**
     * 交换随机码
     * @param request
     * @param response
     * @return
     * @throws Throwable
     */
    @RequestMapping(value = "handshake", method = RequestMethod.POST)
    @ResponseBody
    public Object handshake(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        try {
            String randomKeyEncrypted = RequestUtil.getText(request);
            Encrypter encrypter = this.apiConfig.getEncrypter();
            String privateKey = this.apiConfig.getPrivateKey();
            if(StringUtils.isEmpty(privateKey)) {
                logger.error("未设置正确的私钥");
                throw Errors.ERROR_SSL.getException();
            }
            RSAPrivateKey priKey = encrypter.getPrivateKey(privateKey);
            // 得到客户端传来的随机码
            String randomKey = encrypter.rsaDecryptByPrivateKey(randomKeyEncrypted, priKey);
            request.getSession().setAttribute(Consts.RANDOM_KEY_NAME, randomKey);
            // 再用随机码进行加密,返回给客户端
            // 客户端如果能解开,说明两边对接成功
            // 后续的数据传输都通过这个随机码进行加解密操作
            String retContent = "0";
            String aesStr = encrypter.aesEncryptToHex(retContent, randomKey);
            
            String data = encrypter.rsaEncryptByPrivateKey(aesStr, priKey);
            
            ApiResult result = new ApiResult();
            result.setCode(Errors.SUCCESS.getCode());
            result.setData(data);
            
            return result;
        }catch (Exception e) {
            logger.error(e.getMessage(), e);
            ApiResult result = new ApiResult();
            result.setCode(Errors.ERROR_SSL.getCode());
            result.setMsg("交互错误");
            return result;
        }
    }

    /**
     * oauth2认证获取code
     * @param request
     * @param resp
     * @return
     * @throws URISyntaxException
     * @throws OAuthSystemException
     */
    @RequestMapping("authorize")
    public Object authorize(HttpServletRequest request,HttpServletResponse resp) throws URISyntaxException, OAuthSystemException {
        OAuthResponse response = oauth2Service.authorize(request, resp, apiConfig);
        if(response == null) {
            return null;
        }
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(new URI(response.getLocationUri()));
        return new ResponseEntity<String>(headers, HttpStatus.valueOf(response.getResponseStatus()));
    }

    /**
     * 通过code获取accessToken
     * @param request
     * @return
     * @throws URISyntaxException
     * @throws OAuthSystemException
     */
    @RequestMapping("accessToken")
    public HttpEntity<?> accessToken(HttpServletRequest request) throws URISyntaxException, OAuthSystemException {
        OAuthResponse response = oauth2Service.accessToken(request, apiConfig);
        return new ResponseEntity<String>(  
                response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); 
    }
    
    /**
     * 捕捉异常
     * @param req
     * @param response
     * @param e
     * @throws Exception
     */
    @ExceptionHandler(value = Throwable.class)
    public void jsonErrorHandler(HttpServletRequest req, HttpServletResponse response, Throwable e) {
        Result result = this.caugthException(e);
        this.responseResult(response, result);
    }
    
    protected Result caugthException(Throwable e) {
        String code = Errors.SYS_ERROR.getCode();
        String msg = e.getMessage();
        Object data = null;
        
        if(e instanceof ApiException) {
            ApiException apiEx = (ApiException)e;
            code = apiEx.getCode();
            msg = apiEx.getMessage();
            data = apiEx.getData();
        }
        
        return this.apiConfig.getResultCreator().createErrorResult(code, msg, data);
    }
    
    /**
     * 初始化apiConfig对象。spring容器加载完毕后触发此方法，因此方法中可以使用被@Autowired注解的对象。
     * @param apiConfig
     */
    protected abstract void initApiConfig(ApiConfig apiConfig);

}
